﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using Tesla.Core;
using Tesla.Graphics;
using Tesla.Graphics.Implementation;
using Tesla.Math;
using Tesla.Util;
using D3D = SlimDX.Direct3D10;
using DXGI = SlimDX.DXGI;
using SDX = SlimDX;

namespace Tesla.Direct3D10.Graphics.Implementation {
    /// <summary>
    /// Concrete Direct3D10 implementation for <see cref="SwapChain"/>.
    /// </summary>
    public sealed class D3D10SwapChainImplementation : SwapChainImplementation {
        private PresentationParameters _presentParams;
        private IntPtr _windowHandle;
        private D3D10Renderer _renderer;
        private D3D.Device _graphicsDevice;
        private DXGI.Factory _factory;
        private DXGI.SwapChain _swapChain;
        private D3D.RenderTargetView _renderTargetView;
        private D3D.Texture2D _backBuffer;
        private D3D.Texture2D _staging;
        private D3D.DepthStencilView _depthStencilView;
        private int _interval;
        private bool _resetting;
        private bool _isFullScreen;

        /// <summary>
        /// Gets the D3D10Renderer that created this implementation.
        /// </summary>
        internal D3D10Renderer Renderer {
            get {
                return _renderer;
            }
        }

        /// <summary>
        /// Gets the D3D10 RenderTargetView.
        /// </summary>
        internal D3D.RenderTargetView D3D10RenderTargetView {
            get {
                return _renderTargetView;
            }
        }

        /// <summary>
        /// Gets the D3D10 DepthStencilView.
        /// </summary>
        internal D3D.DepthStencilView D3D10DepthStencilView {
            get {
                return _depthStencilView;
            }
        }

        /// <summary>
        /// Gets the DXGI SwapChain
        /// </summary>
        internal DXGI.SwapChain DXGISwapChain {
            get {
                return _swapChain;
            }
        }

        /// <summary>
        /// Gets the DXGI Factory that created the swap chain.
        /// </summary>
        internal DXGI.Factory DXGIFactory {
            get {
                return _factory;
            }
        }

        /// <summary>
        /// Gets the presentation parameters the swap chain is set to.
        /// </summary>
        public override PresentationParameters PresentationParameters {
            get {
                return _presentParams;
            }
        }

        /// <summary>
        /// Gets if the swap chain is in full screen mode or not. By default, swap chains
        /// are not in full screen.
        /// </summary>
        public override bool IsFullScreen {
            get {
                return _isFullScreen;
            }
        }

        /// <summary>
        /// Gets the window handle the swap chain is bound to.
        /// </summary>
        public override IntPtr WindowHandle {
            get {
                return _windowHandle;
            }
        }

        /// <summary>
        /// Gets if the current display mode is in wide screen or not.
        /// </summary>
        public override bool IsWideScreen {
            get {
                DisplayMode mode = CurrentDisplayMode;
                return mode.AspectRatio > 1.6f;
            }
        }

        /// <summary>
        /// Gets the current display mode of the swap chain.
        /// </summary>
        public override DisplayMode CurrentDisplayMode {
            get {
                DXGI.ModeDescription modeDesc = _swapChain.Description.ModeDescription;
                return new DisplayMode(modeDesc.Width, modeDesc.Height, D3D10Helper.FromD3DSurfaceFormat(modeDesc.Format));
            }
        }

        /// <summary>
        /// Gets the handle to the monitor that contains the majority of the output.
        /// </summary>
        public override IntPtr MonitorHandle {
            get {
                DXGI.Output output = _swapChain.ContainingOutput;
                IntPtr ptr = output.Description.MonitorHandle;
                output.Dispose();
                return ptr;
            }
        }

        /// <summary>
        /// Creates a new instance of <see cref="D3D10SwapChainImplementation"/>.
        /// </summary>
        /// <param name="renderer">The D3D10 renderer.</param>
        /// <param name="factory">The DXGI factory.</param>
        /// <param name="windowHandle">The window handle.</param>
        /// <param name="presentParams">The presentation parameters to initialize to.</param>
        internal D3D10SwapChainImplementation(D3D10Renderer renderer, DXGI.Factory factory, IntPtr windowHandle, PresentationParameters presentParams) {
            _renderer = renderer;
            _graphicsDevice = _renderer.GraphicsDevice;
            _factory = factory;
            _resetting = false;
            _isFullScreen = false;
            _presentParams = presentParams;
            _windowHandle = windowHandle;
            CreateSwapChain(_windowHandle, _presentParams);
            CreateViews();
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="D3D10SwapChainImplementation"/> is reclaimed by garbage collection.
        /// </summary>
        ~D3D10SwapChainImplementation() {
            Dispose(false);
        }

        private void CreateSwapChain(IntPtr windowHandle, PresentationParameters presentParams) {
            DXGI.Format surfaceFormat = D3D10Helper.ToD3DSurfaceFormat(presentParams.BackBufferFormat);

            switch(presentParams.PresentInterval) {
                case PresentInterval.Immediate:
                    _interval = 0;
                    break;
                case PresentInterval.One:
                    _interval = 1;
                    break;
                case PresentInterval.Two:
                    _interval = 2;
                    break;
            }

            DXGI.SampleDescription sampleDesc = new DXGI.SampleDescription(1, 0);
            bool msEnabled = false;
            if(presentParams.MultiSampleCount > 1) {
                sampleDesc.Count = presentParams.MultiSampleCount;
                int q = _graphicsDevice.CheckMultisampleQualityLevels(surfaceFormat, presentParams.MultiSampleCount);
                if(presentParams.MultiSampleQuality < q) {
                    sampleDesc.Quality = presentParams.MultiSampleQuality;
                } else {
                    sampleDesc.Quality = q - 1;
                }
                msEnabled = true;
            }

            DXGI.SwapEffect swap;
            if(presentParams.RenderTargetUsage == RenderTargetUsage.PlatformDefault) {
                if(msEnabled) {
                    swap = DXGI.SwapEffect.Discard;
                } else {
                    swap = DXGI.SwapEffect.Sequential;
                }
            } else if(presentParams.RenderTargetUsage == RenderTargetUsage.DiscardContents) {
                swap = DXGI.SwapEffect.Discard;
            } else {
                swap = DXGI.SwapEffect.Sequential;
            }
            DXGI.ModeDescription modeDesc = new DXGI.ModeDescription(presentParams.BackBufferWidth, presentParams.BackBufferHeight, new SDX.Rational(60, 1), surfaceFormat);
            modeDesc.Scaling = DXGI.DisplayModeScaling.Stretched;
            _swapChain = new DXGI.SwapChain(_factory, _graphicsDevice, new DXGI.SwapChainDescription {
                BufferCount = 1,
                Flags = DXGI.SwapChainFlags.AllowModeSwitch,
                IsWindowed = true,
                ModeDescription = modeDesc,
                OutputHandle = windowHandle,
                SampleDescription = sampleDesc,
                SwapEffect = swap,
                Usage = DXGI.Usage.RenderTargetOutput
            });

            //Add to tracker
            _renderer.Resources.AddTrackedObject(_swapChain.ComPointer, this);

            _factory.SetWindowAssociation(windowHandle, DXGI.WindowAssociationFlags.IgnoreAll | DXGI.WindowAssociationFlags.IgnoreAltEnter);
        }

        private void DestroySwapChain() {
            if(_swapChain != null) {
                if(_isFullScreen) {
                    _swapChain.SetFullScreenState(false, null);
                }
                if(_renderer != null) {
                    _renderer.Resources.RemoveTrackedObject(_swapChain.ComPointer);
                }
                _swapChain.Dispose();
                _swapChain = null;
            }
        }

        private void DestroyViews() {
            if(_renderTargetView != null) {
                if(_renderer != null) {
                    _renderer.Resources.RemoveTrackedObject(_renderTargetView.ComPointer);
                }
                _renderTargetView.Dispose();
                _renderTargetView = null;
            }

            if(_backBuffer != null) {
                if(_renderer != null) {
                    _renderer.Resources.RemoveTrackedObject(_backBuffer.ComPointer);
                }
                _backBuffer.Dispose();
                _backBuffer = null;
            }

            if(_staging != null) {
                if(_renderer != null) {
                    _renderer.Resources.RemoveTrackedObject(_staging.ComPointer);
                }
                _staging.Dispose();
                _staging = null;
            }

            if(_depthStencilView != null) {
                if(_renderer != null) {
                    _renderer.Resources.RemoveTrackedObject(_depthStencilView.ComPointer);
                }
                _depthStencilView.Dispose();
                _depthStencilView = null;
            }
        }

        private void CreateViews() {
            _backBuffer = D3D.Resource.FromSwapChain<D3D.Texture2D>(_swapChain, 0);
            _renderTargetView = new D3D.RenderTargetView(_graphicsDevice, _backBuffer);

            //Add resources to tracker
            _renderer.Resources.AddTrackedObject(_backBuffer.ComPointer, this);
            _renderer.Resources.AddTrackedObject(_renderTargetView.ComPointer, this);

            if(_presentParams.DepthStencilFormat != DepthFormat.None) {
                D3D.Texture2DDescription depthBufferDesc = new D3D.Texture2DDescription {
                    ArraySize = 1,
                    BindFlags = D3D.BindFlags.DepthStencil,
                    CpuAccessFlags = D3D.CpuAccessFlags.None,
                    Format = D3D10Helper.ToD3DDepthFormat(_presentParams.DepthStencilFormat),
                    Height = _presentParams.BackBufferHeight,
                    Width = _presentParams.BackBufferWidth,
                    MipLevels = 1,
                    OptionFlags = D3D.ResourceOptionFlags.None,
                    SampleDescription = _backBuffer.Description.SampleDescription,
                    Usage = D3D.ResourceUsage.Default
                };

                using(var depthBuffer = new D3D.Texture2D(_graphicsDevice, depthBufferDesc)) {
                    _depthStencilView = new D3D.DepthStencilView(_graphicsDevice, depthBuffer);

                    //Add resource to tracker
                    _renderer.Resources.AddTrackedObject(_depthStencilView.ComPointer, this);
                }
            }
        }

        /// <summary>
        /// Sets the swapchain's backbuffer as the active render target and clears it.
        /// </summary>
        /// <param name="options">Clear option flags defining which buffer to clear.</param>
        /// <param name="color">Color to clear the color buffer to</param>
        /// <param name="depth">Depth value to clear the depth buffer to</param>
        /// <param name="stencil">Stencil value to clear the stencil buffer to</param>
        public override void Clear(ClearOptions options, Color color, float depth, int stencil) {
            if(_resetting) {
                return;
            }

            SlimDX.Color4 sc;
            D3D10Helper.Convert(ref color, out sc);

            _renderer.TargetManager.SetActiveSwapChain(this);

            if(_depthStencilView != null) {
                _graphicsDevice.OutputMerger.SetTargets(_depthStencilView, _renderTargetView);
                if(((options & ClearOptions.Depth) == ClearOptions.Depth) &&((options & ClearOptions.Stencil) == ClearOptions.Stencil)) {
                    _graphicsDevice.ClearDepthStencilView(_depthStencilView, D3D.DepthStencilClearFlags.Depth | D3D.DepthStencilClearFlags.Stencil, depth, (byte) stencil);
                } else if((options & ClearOptions.Depth) == ClearOptions.Depth) {
                    _graphicsDevice.ClearDepthStencilView(_depthStencilView, D3D.DepthStencilClearFlags.Depth, depth, (byte) stencil);
                } else if((options & ClearOptions.Stencil) == ClearOptions.Stencil) {
                    _graphicsDevice.ClearDepthStencilView(_depthStencilView, D3D.DepthStencilClearFlags.Stencil, depth, (byte) stencil);
                }
                if((options & ClearOptions.Target) == ClearOptions.Target) {
                    _graphicsDevice.ClearRenderTargetView(_renderTargetView, sc);
                }
            } else {
                _graphicsDevice.OutputMerger.SetTargets(_renderTargetView);
                if((options & ClearOptions.Target) == ClearOptions.Target) {
                    _graphicsDevice.ClearRenderTargetView(_renderTargetView, sc);
                }
            }
        }

        private void CheckRectangle(Rectangle rect, int width, int height) {
            int x = rect.X;
            int y = rect.Y;
            int rectWidth = rect.Width;
            int rectHeight = rect.Height;
            if(rectWidth <= 0 || rectHeight <= 0) {
                throw new ArgumentOutOfRangeException("subimage", "Rectangle's width and height must be greater than zero.");
            }
            if(x + rectWidth > width || y + rectHeight > height) {
                throw new ArgumentOutOfRangeException("subimage", "Subimage dimensions do not match subresource dimensions");
            }
            if(x < 0 || x > width) {
                throw new ArgumentOutOfRangeException("subimage", "Invalid top-left x coordinate for subimage");
            }
            if(y < 0 || y > height) {
                throw new ArgumentOutOfRangeException("subimage", "Invalid top-left y coordinate for subimage");
            }
        }

        private void CreateStaging() {
            if(_staging == null) {
                D3D.Texture2DDescription desc = _backBuffer.Description;
                desc.BindFlags = D3D.BindFlags.None;
                desc.CpuAccessFlags = D3D.CpuAccessFlags.Write | D3D.CpuAccessFlags.Read;
                desc.Usage = D3D.ResourceUsage.Staging;

                _staging = new D3D.Texture2D(_graphicsDevice, desc);

                //Add to tracker
                _renderer.Resources.AddTrackedObject(_staging.ComPointer, this);
            }
        }

        /// <summary>
        /// Gets the back buffer data.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="subimage">The rectangle representing the sub-image. Null value means to grab the whole back buffer.</param>
        /// <param name="data">The data.</param>
        /// <param name="startIndex">The starting index in the array.</param>
        /// <param name="elementCount">The number of eleemnts to read</param>
        public override void GetBackBufferData<T>(Rectangle? subimage, T[] data, int startIndex, int elementCount) {
            if(_backBuffer.Disposed) {
                throw new ObjectDisposedException(GetType().Name);
            }

            //Throws null or out of range exception
            D3D10Helper.CheckArrayBounds(data, startIndex, elementCount);

            //Calc subresource dimensions 
            int width = _presentParams.BackBufferWidth;
            int height = _presentParams.BackBufferHeight;

            //Get the dimensions, if its null we copy the whole texture
            Rectangle rect;
            if(subimage.HasValue) {
                rect = subimage.Value;
                CheckRectangle(rect, width, height);
            } else {
                rect = new Rectangle(0, 0, width, height);
            }

            if(elementCount > rect.Width * rect.Height) {
                throw new ArgumentOutOfRangeException("elementCount", "Number of elements to read is larger than contained in the specified subimage.");
            }

            //Create staging
            CreateStaging();

            try {
                int row = rect.Y;
                int col = rect.X;
                int rectWidth = rect.Width;
                int rectHeight = rect.Height;

                //Do resource copy
                if(row == 0 && col == 0 && rectWidth == width && rectHeight == height) {
                    _graphicsDevice.CopyResource(_backBuffer, _staging);
                } else {
                    D3D.ResourceRegion region = new D3D.ResourceRegion();
                    region.Left = col;
                    region.Right = col + rectWidth;
                    region.Top = row;
                    region.Bottom = row + rectHeight;
                    region.Front = 0;
                    region.Back = 1;
                    _graphicsDevice.CopySubresourceRegion(_backBuffer, 0, region, _staging, 0, col, row, 0);
                }

                SDX.DataRectangle dataRect = _staging.Map(0, D3D.MapMode.Read, D3D.MapFlags.None);
                SDX.DataStream ds = dataRect.Data;
                int elemSize = MemoryHelper.SizeOf<T>();
                int formatSize = D3D10Helper.FormatSize(_presentParams.BackBufferFormat);
                int pitch = dataRect.Pitch;

                int index = startIndex;
                int count = elementCount;

                //Compute the actual number of elements based on the format size and the element size we're copying the bytes into
                int actWidth = (int) MathHelper.Floor(rectWidth * (formatSize / elemSize));
                //Go row by row
                for(int i = row; i < height; i++) {
                    //Set the position
                    ds.Position = (i * pitch) + (col * formatSize);
                    //Break if we've run out of elements or on the very last row that isn't complete
                    if(count <= 0) {
                        break;
                    } else if(count < actWidth) {
                        ds.ReadRange<T>(data, index, count);
                        break;
                    }
                    //Otherwise, copy the whole row and increment/decrement the index and count
                    ds.ReadRange<T>(data, index, actWidth);
                    index += actWidth;
                    count -= actWidth;
                }
                _staging.Unmap(0);
            } catch(Exception e) {
                throw new TeslaException("Error reading from D3D10 Texture2D.", e);
            }
        }

        /// <summary>
        /// Presents the contents of the back buffer to the screen, and flips the front/back buffers.
        /// </summary>
        public override void Present() {
            //If resetting, return return return
            if(_resetting) {
                return;
            }
            _swapChain.Present(_interval, DXGI.PresentFlags.None);
        }

        /// <summary>
        /// Toggles the swap chain to full screen mode.
        /// </summary>
        public override void ToggleFullScreen() {
            _resetting = true;
            _isFullScreen = !_isFullScreen;
            _swapChain.IsFullScreen = _isFullScreen;
            //Resize targets
            if(_isFullScreen) {
                DXGI.ModeDescription modeDesc = new DXGI.ModeDescription();
                modeDesc.Format = D3D10Helper.ToD3DSurfaceFormat(_presentParams.BackBufferFormat);
                modeDesc.Width = _presentParams.BackBufferWidth;
                modeDesc.Height = _presentParams.BackBufferHeight;
                modeDesc.RefreshRate = new SDX.Rational(0, 0);
                _swapChain.ResizeTarget(modeDesc);
            }
            _resetting = false;
        }

        /// <summary>
        /// Resets the swapchain.
        /// </summary>
        /// <param name="windowHandle">The new window handle to bind to</param>
        /// <param name="presentParams">The new presentation parameters to use</param>
        public override void Reset(IntPtr windowHandle, PresentationParameters presentParams) {
            _resetting = true;

            DestroyViews();
            _presentParams = presentParams;
            _windowHandle = windowHandle;
            CreateSwapChain(_windowHandle, _presentParams);
            CreateViews();

            _resetting = false;
        }

        /// <summary>
        /// Resizes the backbuffer.
        /// </summary>
        /// <param name="width">Width of the back buffer</param>
        /// <param name="height">Height of the back buffer</param>
        public override void Resize(int width, int height) {
            //Do not resize if the dimensions have not changed
            if(_presentParams.BackBufferHeight == height && _presentParams.BackBufferWidth == width) {
                return;
            }
            _resetting = true;
            _presentParams.BackBufferWidth = width;
            _presentParams.BackBufferHeight = height;
            
            //If resizing in full screen, resize the target/front buffer otherwise bad things seem to happen
            if(_isFullScreen) {
                DXGI.ModeDescription modeDesc = new DXGI.ModeDescription();
                modeDesc.Format = D3D10Helper.ToD3DSurfaceFormat(_presentParams.BackBufferFormat);
                modeDesc.Width = width;
                modeDesc.Height = height;
                modeDesc.RefreshRate = new SDX.Rational(0, 0);
                _swapChain.ResizeTarget(modeDesc);
            }

            //Resize the swap chain back buffer
            DestroyViews();
            _swapChain.ResizeBuffers(1, width, height, D3D10Helper.ToD3DSurfaceFormat(_presentParams.BackBufferFormat), DXGI.SwapChainFlags.AllowModeSwitch);
            CreateViews();

            _resetting = false;
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected override void Dispose(bool disposing) {
            if(!IsDisposed) {
                //Dispose of managed resources
                if(disposing) {
                    DestroyViews();
                    DestroySwapChain();
                }
            }
            base.Dispose(disposing);
        }
    }
}
